package gu.sql2java;

import java.util.Arrays;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.DeepCacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.UncheckedExecutionException;

import static gu.sql2java.Managers.getBaseTableManager;
import gu.sql2java.exception.ObjectRetrievalException;
import gu.sql2java.exception.RuntimeDaoException;

import static com.google.common.base.Preconditions.*;
import static gu.sql2java.SimpleLog.*;
import static com.google.common.base.MoreObjects.*;

/**
 * 基于 {@link LoadingCache}实现表数据缓存,并可以通过{@link TableListener}实现缓存数据自动更新<br>
 * 支持一个或多个column组成的唯一索引
 * @author guyadong
 *
 * @param <B> 数据库记录对象类型(Java Bean)
 */
class ColumnCache<B extends BaseBean> implements IKeyCache<B>,RemovalListener<Object[],B>,Constant {
	protected final RowMetaData metaData;
	private final LoadingCache<Object[], B> cache;
    private final ConcurrentMap<Object[], B> cacheMap;
    /** 当前更新策略 */
    protected final UpdateStrategy updateStrategy;
    protected final Long maximumSize;
    protected final long duration;
    protected final TimeUnit unit;
	private final int[] keyIds;
	protected final BaseTableManager<B> manager;
	protected final String indexName;
	protected static boolean debug = false;	
    /**
     * 构造函数
     * @param metaData meta data for table
     * @param indexName index name , as primary key if {@code null}
     * @param updateStrategy cache update strategy,{@link Constant#DEFAULT_STRATEGY} be used if {@code null}
     * @param maximumSize maximum capacity of cache ,{@link Constant#DEFAULT_CACHE_MAXIMUMSIZE } be used if {@code null} or <=0,see also {@link CacheBuilder#maximumSize(long)}
     * @param duration cache data expired time,{@link Constant#DEFAULT_DURATION} be used if {@code null} or <=0,see also {@link CacheBuilder#expireAfterAccess(long, TimeUnit)}
     * @param unit time unit for {@code duration},{@link Constant#DEFAULT_TIME_UNIT} be used if {@code null},see also {@link CacheBuilder#expireAfterAccess(long, TimeUnit)}
     */
    ColumnCache(RowMetaData metaData,String indexName,UpdateStrategy updateStrategy, Long maximumSize, Long duration, TimeUnit unit) {
    	this.metaData = checkNotNull(metaData,"metaData is null");
		this.manager = getBaseTableManager(metaData.tablename);
		this.indexName = Strings.emptyToNull(indexName);
    	if(this.indexName == null){
    		this.keyIds = metaData.primaryKeyIds;
    	}else{
    		this.keyIds = metaData.indexIdArray(indexName);
    	}

        this.updateStrategy = firstNonNull(updateStrategy, DEFAULT_STRATEGY);
        this.maximumSize = (maximumSize != null && maximumSize > 0) ? maximumSize : DEFAULT_CACHE_MAXIMUMSIZE;
        this.duration = (duration != null && duration > 0) ? duration : DEFAULT_DURATION;
        this.unit = firstNonNull(unit, DEFAULT_TIME_UNIT);
        cache = DeepCacheBuilder.newBuilder()
            .maximumSize(this.maximumSize)
            .expireAfterAccess(this.duration, this.unit)
            .removalListener(this)
            .build(
                new CacheLoader<Object[],B>() {
                    @Override
                    public B load(Object[] keys) throws Exception {
                    	return loadfromDatabase(keys);
                    }});
        cacheMap = cache.asMap();
        if(debug){
	    	log("ColumnCache FOR %s(%s) of %s(%s)",
	    		firstNonNull(indexName, "PK"),
	    		Joiner.on(",").join(metaData.columnNamesOf(keyIds)),        		
	    		metaData.tablename,
	    		updateStrategy);
        }
    }
    /**
     * @param objects
     * @return first index of element that not null if exists,or return -1 if not found,or -2 if objects is null 
     */
    private static int indexOfFirstNull(Object...objects) {
		if(objects != null){
			for(int i = 0; i < objects.length; ++i){
				if(null == objects[i]){
					return i;
				}
			}
			return -1;
		}
		return -2;
	}
    /**
	 * @param objects object array
	 * @return true if objects is null or any element in objects is null 
	 */
	private static boolean hasNull(Object...objects) {
		return indexOfFirstNull(objects) != -1;
	}
    /**
     * check the keys's value is valid for index or primary key 
     * @param keys array of key value
     * @throws ObjectRetrievalException has null element in keys
     */
    private void checkNonNullKey(Object...keys)throws ObjectRetrievalException{
    	checkArgument(keys != null && keys.length == keyIds.length,
    			"MISMATCHED length of 'keys' with column count of %s",				
				firstNonNull(indexName, "PK"));
    	int index = indexOfFirstNull(keys);
    	if(index != -1){
    		throw new ObjectRetrievalException(String.format("value of %s is null", metaData.columnNames.get(keyIds[index])));
    	}
    }
    /**
     * 从数据库中加载外键指定的记录,没有找到指定的记录则抛出异常{@link ObjectRetrievalException}<br>
     * @param keys
     * @return B 
     * @throws RuntimeDaoException
     * @throws ObjectRetrievalException
     */
    protected B loadfromDatabase(Object[] keys) throws RuntimeDaoException, ObjectRetrievalException{

    	ImmutableMap.Builder<Integer, Object> builder = ImmutableMap.builder();
    	for(int i = 0 ; i < keyIds.length; ++i){
    		builder.put(keyIds[i], keys[i]);
    	}
    	B bean = manager.createBean().copy(builder.build());
    	if(debug){
    		log("LOAD BY %s%s of %s",firstNonNull(indexName, "PK"),Arrays.toString(keys),metaData.tablename);
    	}
    	try {
    		return manager.loadUniqueUsingTemplateChecked(bean);	
		} catch (ObjectRetrievalException e) {
			throw new ObjectRetrievalException(logString("Not found element for {}{} of {}",
					firstNonNull(indexName, "PK"),Arrays.toString(keys),metaData.tablename));
		}
    	
    }
	@Override
	public B getBean(Object... keys)throws ObjectRetrievalException{
    	checkNonNullKey(keys);
    	try {
    		return cache.get(keys);
    	}catch(ExecutionException | UncheckedExecutionException e){
    		if(null != e.getCause()){
    			Throwables.throwIfInstanceOf(e.getCause(), ObjectRetrievalException.class);
    			Throwables.throwIfUnchecked(e.getCause());
    		}
    		Throwables.throwIfUnchecked(e);
    		throw new RuntimeException(e);
    	}
    }

    @Override
	public B getBeanUnchecked(Object... keys){
        try{
            return getBean(keys);
        }catch(ObjectRetrievalException e){
            return null;
        }        
    }
    @Override
    public boolean hasValidKey(B bean){
    	if(bean != null){
    		for(Object key : bean.asValueArray(keyIds)){
    			if(key == null){
    				return false;
    			}
    		}
    		return true;
    	}
    	return false;
    }
    @Override
    public B remove(B bean){
    	if(bean != null){
    		Object[] keys = bean.asValueArray(keyIds);
    		return cacheMap.remove(keys);
    	}
    	return null;
    }

    @Override
    public void update(B bean, UpdateStrategy updateStrategy){
    	if(bean != null){
    		updateStrategy = firstNonNull(updateStrategy, UpdateStrategy.always);
    		Object[] keys = bean.asValueArray(keyIds);
    		if(!hasNull(keys)){
    			switch (updateStrategy) {
    			case replace:
    				cacheMap.replace(keys, bean);
    				break;
    			case remove:
    				cacheMap.remove(keys);
    				break;
    			case refresh:
    				cacheMap.put(keys,loadfromDatabase(keys));
    				break;
    			case always:
    			default:
    				cacheMap.put(keys, bean);
    				break;
    			}
    			if(debug){
    				log("UPDATE(%s) RECORD %s%s of %s",
    						updateStrategy,
    						firstNonNull(indexName, "PK"),
    						Arrays.toString(keys),
    						metaData.tablename);
    			}
    		}
    	}
    }

    @Override
    public void update(B bean){
    	update(bean, updateStrategy);
    }

    @Override
	public void onRemoval(RemovalNotification<Object[], B> notification) {
		if(debug){
			log("CACHE REMOVE:Key:{}({}) for {}", 
					firstNonNull(indexName, "PK"),
					Arrays.toString(notification.getKey()),
					metaData.tablename);
		}
	}
	/**
	 * @return native manager
	 */
	public BaseTableManager<B> getManager() {
		return manager;
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((indexName == null) ? 0 : indexName.hashCode());
		result = prime * result + Arrays.hashCode(keyIds);
		result = prime * result + ((metaData == null) ? 0 : metaData.hashCode());
		result = prime * result + ((updateStrategy == null) ? 0 : updateStrategy.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof ColumnCache)) {
			return false;
		}
		ColumnCache<?> other = (ColumnCache<?>) obj;
		if (indexName == null) {
			if (other.indexName != null) {
				return false;
			}
		} else if (!indexName.equals(other.indexName)) {
			return false;
		}
		if (!Arrays.equals(keyIds, other.keyIds)) {
			return false;
		}
		if (metaData == null) {
			if (other.metaData != null) {
				return false;
			}
		} else if (!metaData.equals(other.metaData)) {
			return false;
		}
		if (updateStrategy != other.updateStrategy) {
			return false;
		}
		return true;
	}
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("ColumnCache [tablename=");
		builder.append(metaData.tablename);
		builder.append(", updateStrategy=");
		builder.append(updateStrategy);
		builder.append(", keyIds=");
		builder.append(metaData.columnNamesOf(keyIds));
		builder.append(", indexName=");
		builder.append(firstNonNull(indexName,"PK"));
		builder.append(", maximumSize=");
		builder.append(maximumSize);
		builder.append(", duration=");
		builder.append(duration);
		builder.append(", unit=");
		builder.append(unit);
		builder.append("]");
		return builder.toString();
	}
	/**
	 * set debug flag that determine if output log message,default : false
	 * @param debug flag for debug message output
	 */
	static void setDebug(boolean debug) {
		ColumnCache.debug = debug;
	}
}